#include "ServerTest.hh"
#include "../Network/FleetColonizePlanet.hh"
#include "../Network/Done.hh"
#include "../Network/FleetMove.hh"
#include "../Network/JoinGame.hh"
#include "../Network/NewGame.hh"
#include "../Network/NextTurn.hh"
#include "../Network/PlayerJoined.hh"
#include "../Network/StateChanged.hh"
#include "../Network/XmlServer.hh"
#include "../Network/XmlSocket.hh"
#include "../World/User.hh"
#include <algorithm>

#define CONNECT_TO_SERVER(SOCKET)                                   \
    {                                                               \
        (SOCKET).connectToHostEncrypted("localhost", DEFAULT_PORT); \
                                                                    \
        if ((SOCKET).state() != QAbstractSocket::ConnectedState)    \
            if (!(SOCKET).waitForEncrypted()) {                     \
                qDebug("%d", (SOCKET).error());                     \
                QFAIL("socket error");                              \
            }                                                       \
    }                                                               \

#define DISCONNECT_FROM_SERVER(SOCKET)                              \
    {                                                               \
        QVERIFY(!(SOCKET).messages_available());                    \
        (SOCKET).disconnectFromHost();                              \
                                                                    \
        if ((SOCKET).state() != QAbstractSocket::UnconnectedState)  \
            QVERIFY((SOCKET).waitForDisconnected());                \
    }                                                               \

#define READ_MESSAGE(T, SOCKET, RESULT)                             \
    {                                                               \
        wait_for_message((SOCKET));                                 \
        spXmlMessage msg = XmlMessage::construct((SOCKET).read());  \
        QVERIFY(msg);                                               \
        (RESULT) = msg.dynamicCast< T >();                          \
        QVERIFY((RESULT));                                          \
    }                                                               \

#define READ_PLAYER_JOINED(SOCKET, NAME)                        \
    {                                                           \
        spPlayerJoined playerJoined;                            \
        READ_MESSAGE(PlayerJoined, (SOCKET), playerJoined);     \
        (NAME) = playerJoined->playerName;                      \
    }                                                           \

#define PLAYER_JOINED(SOCKET, NAME)         \
    {                                       \
        QString name;                       \
        READ_PLAYER_JOINED((SOCKET), name); \
        QCOMPARE(name, QString((NAME)));    \
    }                                       \

#define NEW_GAME_EL(SOCKET, NAME, MIN, MAX, E_STATUS, E_MESSAGE)    \
    {                                                               \
        NewGame newGame((NAME), (MIN), (MAX));                      \
        newGame.send_to((SOCKET));                                  \
        spDone done;                                                \
        READ_MESSAGE(Done, (SOCKET), done);                         \
        QCOMPARE(done->status, (E_STATUS));                         \
        QCOMPARE(done->message, QString((E_MESSAGE)));              \
    }                                                               \

#define NEW_GAME_E(SOCKET, NAME, E_STATUS, E_MESSAGE)                       \
    NEW_GAME_EL((SOCKET), (NAME), 2, UNLIMITED, (E_STATUS), (E_MESSAGE));   \

#define NEW_GAME_L(SOCKET, NAME, MIN, MAX)                                  \
    NEW_GAME_EL((SOCKET), (NAME), (MIN), (MAX), true, "init.new_game ok");  \

#define NEW_GAME(SOCKET, NAME)                  \
    NEW_GAME_L((SOCKET), (NAME), 2, UNLIMITED); \

#define JOIN_GAME_E(SOCKET, NAME, E_STATUS, E_MESSAGE)  \
    {                                                   \
        JoinGame joinGame((NAME));                      \
        joinGame.send_to((SOCKET));                     \
        spDone done;                                    \
        READ_MESSAGE(Done, (SOCKET), done);             \
        QCOMPARE(done->status, (E_STATUS));             \
        QCOMPARE(done->message, QString((E_MESSAGE)));  \
    }                                                   \

#define JOIN_GAME(SOCKET, NAME)                                 \
    JOIN_GAME_E((SOCKET), (NAME), true, "init.join_game ok");   \

#define NEXT_TURN_E(SOCKET, E_STATUS, E_MESSAGE)        \
    {                                                   \
        NextTurn nextTurn;                              \
        nextTurn.send_to((SOCKET));                     \
        spDone done;                                    \
        READ_MESSAGE(Done, (SOCKET), done);             \
        QCOMPARE(done->status, (E_STATUS));             \
        QCOMPARE(done->message, QString((E_MESSAGE)));  \
    }                                                   \

#define NEXT_TURN(SOCKET) NEXT_TURN_E((SOCKET), true, "game.next_turn ok");

#define GET_FIRST_FLEET(MESSAGE, PLAYER)                            \
    (*(MESSAGE)->galaxyInfo.players.value((PLAYER)).fleets.begin()) \

using namespace Geom;
using namespace Network;
using namespace Test;
using namespace QTest;
using namespace std;
using namespace World;

ServerTest::ServerTest():
    QObject(),
    server(),
    sslConfig()
{
    QString const keys[] = {
      "ssl/client_foo.key",
      "ssl/client_bar.key",
      "ssl/client_baz.key",
      "ssl/client_fuz.key"
    };
    QString const certs[] = {
      "ssl/client_foo.crt",
      "ssl/client_bar.crt",
      "ssl/client_baz.crt",
      "ssl/client_fuz.crt"
    };
    QString const caPath = "ssl/ca/ca.crt";

    for (u1 i = 0; i < 4; ++i) {
        sslConfig[i] = XmlSocket::ssl_config(keys[i], certs[i], caPath);
    }
}

void ServerTest::wait_for_message(XmlSocket& socket) {
    while (!socket.messages_available())
        qWait(1);
}

void ServerTest::init() {
    server.setReadChannel(QProcess::StandardError);
    server.start("./lprog server 55555 -ssl-conf ssl_server.conf");

    if (server.state() != QProcess::Running)
        QVERIFY(server.waitForStarted());

    server.waitForReadyRead();
    QByteArray line = server.readLine();
    QCOMPARE(QString(line), QString("listening on port 55555\n"));
}

void ServerTest::cleanup() {
    if (server.state() != QProcess::Running)
        QFAIL("server crashed");

    server.terminate();

    if (server.state() != QProcess::NotRunning && !server.waitForFinished()) {
        server.kill();

        if (server.state() != QProcess::NotRunning)
            QVERIFY(server.waitForFinished());
    }
}

void ServerTest::connect() {
    XmlSocket socket(this, sslConfig[0]);
    CONNECT_TO_SERVER(socket);
    DISCONNECT_FROM_SERVER(socket);
}

void ServerTest::new_game_ok() {
    XmlSocket socket(this, sslConfig[0]);
    CONNECT_TO_SERVER(socket);
    NEW_GAME(socket, "xxx");
    DISCONNECT_FROM_SERVER(socket);
}

void ServerTest::new_game_fail() {
    XmlSocket sockets[2];
    sockets[0].setSslConfiguration(sslConfig[0]);
    sockets[1].setSslConfiguration(sslConfig[1]);

    for (u1 i = 0; i < 2; ++i) {
        CONNECT_TO_SERVER(sockets[i]);
        NEW_GAME_E(
          sockets[i],
          "xxx",
          !i,
          i ? "init.new_game failed already exists" : "init.new_game ok"
        );
    }

    for (u1 i = 0; i < 2; ++i)
        DISCONNECT_FROM_SERVER(sockets[i]);
}

void ServerTest::join_game_ok() {
    XmlSocket sockets[2];
    sockets[0].setSslConfiguration(sslConfig[0]);
    sockets[1].setSslConfiguration(sslConfig[1]);
    QString const users[] = {"foo", "bar"};
    CONNECT_TO_SERVER(sockets[0]);
    CONNECT_TO_SERVER(sockets[1]);
    NEW_GAME(sockets[0], "xxx");
    JOIN_GAME(sockets[1], "xxx");
    PLAYER_JOINED(sockets[0], users[1]);
    PLAYER_JOINED(sockets[1], users[0]);
    DISCONNECT_FROM_SERVER(sockets[0]);
    DISCONNECT_FROM_SERVER(sockets[1]);
}

void ServerTest::join_game_fail_not_found() {
    XmlSocket socket(this, sslConfig[1]);
    CONNECT_TO_SERVER(socket);
    JOIN_GAME_E(socket, "xxx", false, "init.join_game failed not found");
    DISCONNECT_FROM_SERVER(socket);
}

void ServerTest::join_game_fail_full() {
    XmlSocket sockets[3];
    sockets[0].setSslConfiguration(sslConfig[0]);
    sockets[1].setSslConfiguration(sslConfig[1]);
    sockets[2].setSslConfiguration(sslConfig[2]);
    QString const users[]     = {"foo", "bar", "baz"};

    for (u1 i = 0; i < 3; ++i) {
        CONNECT_TO_SERVER(sockets[i]);
    }

    NEW_GAME_L(sockets[0], "qq", 2, 2);
    JOIN_GAME(sockets[1], "qq");
    JOIN_GAME_E(sockets[2], "qq", false, "init.join_game failed full");
    PLAYER_JOINED(sockets[0], users[1]);
    PLAYER_JOINED(sockets[1], users[0]);

    for (u1 i = 0; i < 3; ++i) {
        DISCONNECT_FROM_SERVER(sockets[i]);
    }
}

void ServerTest::start_game() {
    XmlSocket sockets[4];
    sockets[0].setSslConfiguration(sslConfig[0]);
    sockets[1].setSslConfiguration(sslConfig[1]);
    sockets[2].setSslConfiguration(sslConfig[2]);
    sockets[3].setSslConfiguration(sslConfig[3]);
    QVector< QString > users;   QVector< QString > passwords;
    users.append("foo");        passwords.append("bar");
    users.append("bar");        passwords.append("baz");
    users.append("baz");        passwords.append("fuz");
    users.append("fuz");        passwords.append("foo");

    // mindenki kapcsolódik és bejelentkezik
    for (u1 i = 0; i < 4; ++i) {
        CONNECT_TO_SERVER(sockets[i]);
    }

    // foo létrehoz egy játékot
    NEW_GAME_L(sockets[0], "xyz", 4, 4);

    // bar, baz és fuz belépnek
    for (u1 i = 1; i < 4; ++i) {
        JOIN_GAME(sockets[i], "xyz");
    }

    // init.player_joined üzenetek ellenőrzése
    for (u1 i = 0; i < 4; ++i) {
        QSet< QString > recvd;
        recvd.insert(users[i]);

        for (u1 j = 0; j < 3; ++j) {
            QString player;
            READ_PLAYER_JOINED(sockets[i], player);
            QVERIFY(users.contains(player));
            QVERIFY(!recvd.contains(player));
            recvd.insert(player);
        }
    }

    // fuz elindítja a játékot és kilép
    NEXT_TURN(sockets[3]);
    DISCONNECT_FROM_SERVER(sockets[3]);

    // foo, bar és baz elindítja a játékot
    for (u1 i = 0; i < 3; ++i) {
        NEXT_TURN(sockets[i]);
    }

    // játék állapot ellenőrzése
    for (u1 i = 0; i < 3; ++i) {
        spStateChanged stateChanged;
        READ_MESSAGE(StateChanged, sockets[i], stateChanged);
        QCOMPARE(stateChanged->galaxyInfo.players.size(), 4);
    }

    // foo, bar és baz kilép
    for (u1 i = 0; i < 3; ++i) {
        DISCONNECT_FROM_SERVER(sockets[i]);
    }
}

void ServerTest::join_game_fail_started() {
    XmlSocket sockets[3];
    sockets[0].setSslConfiguration(sslConfig[0]);
    sockets[1].setSslConfiguration(sslConfig[1]);
    sockets[2].setSslConfiguration(sslConfig[2]);

    // mindenki kapcsolódik és bejelentkezik
    for (u1 i = 0; i < 3; ++i) {
        CONNECT_TO_SERVER(sockets[i]);
    }

    NEW_GAME(sockets[0], "xyz");
    JOIN_GAME(sockets[1], "xyz");
    {
        spPlayerJoined playerJoined;
        READ_MESSAGE(PlayerJoined, sockets[0], playerJoined);
        READ_MESSAGE(PlayerJoined, sockets[1], playerJoined);
    }
    NEXT_TURN(sockets[0]);
    NEXT_TURN(sockets[1]);
    JOIN_GAME_E(
      sockets[2], "xyz",
      false, "init.join_game failed already started"
    );
    {
        spStateChanged stateChanged;
        READ_MESSAGE(StateChanged, sockets[0], stateChanged);
        READ_MESSAGE(StateChanged, sockets[1], stateChanged);
    }

    // mindenki kilép
    for (u1 i = 0; i < 3; ++i) {
        DISCONNECT_FROM_SERVER(sockets[i]);
    }
}

void ServerTest::move_fleet() {
    XmlSocket sockets[2];
    sockets[0].setSslConfiguration(sslConfig[0]);
    sockets[1].setSslConfiguration(sslConfig[1]);
    QString const users[] = {"foo", "bar"};

    // Játék elindítása
    CONNECT_TO_SERVER(sockets[0]);
    CONNECT_TO_SERVER(sockets[1]);
    NEW_GAME(sockets[0], "xxx");
    JOIN_GAME(sockets[1], "xxx");
    PLAYER_JOINED(sockets[0], users[1]);
    PLAYER_JOINED(sockets[1], users[0]);
    NEXT_TURN(sockets[0]);
    NEXT_TURN(sockets[1]);

    // Kezdő állapot lekérdezése
    spStateChanged stateChanged;
    READ_MESSAGE(StateChanged, sockets[0], stateChanged);
    QCOMPARE(stateChanged->galaxyInfo.players.size(), 2);
    StateChanged::FleetInfo fleet = GET_FIRST_FLEET(stateChanged, users[0]);
    Vector< f8 > position = fleet.position;
    READ_MESSAGE(StateChanged, sockets[1], stateChanged);

    // Flotta mozgatása a (0,0) pontba
    {
        FleetMove fleetMove(fleet.id, Vector<>(0.0, 0.0));
        fleetMove.send_to(sockets[0]);
        spDone done;
        READ_MESSAGE(Done, sockets[0], done);
        QCOMPARE(done->status, true);
        QCOMPARE(done->message, QString("game.fleet_move ok"));
    }

    // Mozgás ellenőrzése
    for (u1 i = 0; i < 10; ++i) {
        NEXT_TURN(sockets[0]);
        NEXT_TURN(sockets[1]);
        spStateChanged stateChanged;
        READ_MESSAGE(StateChanged, sockets[0], stateChanged);
        fleet = GET_FIRST_FLEET(stateChanged, users[0]);
        QVERIFY(fleet.position.get_length() < position.get_length());
        position = fleet.position;
        READ_MESSAGE(StateChanged, sockets[1], stateChanged);
    }

    DISCONNECT_FROM_SERVER(sockets[0]);
    DISCONNECT_FROM_SERVER(sockets[1]);
}

struct Closer {
    Vector< f8 > position;

    bool
    operator () (
      StateChanged::StarInfo const& a,
      StateChanged::StarInfo const& b
    ) {
    	if (a.planets.size() == 0)
    		return false;

    	if (b.planets.size() == 0)
			return true;

        f8 da = (a.position - position).get_length();
        f8 db = (b.position - position).get_length();
        return da < db;
	}
};

void ServerTest::colonize_planet() {
    XmlSocket sockets[2];
    sockets[0].setSslConfiguration(sslConfig[0]);
    sockets[1].setSslConfiguration(sslConfig[1]);
    QString const users[] = {"foo", "bar"};

    // Játék elindítása
    CONNECT_TO_SERVER(sockets[0]);
    CONNECT_TO_SERVER(sockets[1]);
    NEW_GAME(sockets[0], "xxx");
    JOIN_GAME(sockets[1], "xxx");
    PLAYER_JOINED(sockets[0], users[1]);
    PLAYER_JOINED(sockets[1], users[0]);
    NEXT_TURN(sockets[0]);
    NEXT_TURN(sockets[1]);

    // Kezdő állapot lekérdezése
    spStateChanged stateChanged;
    READ_MESSAGE(StateChanged, sockets[0], stateChanged);
    QCOMPARE(stateChanged->galaxyInfo.players.size(), 2);
    StateChanged::FleetInfo fleet = GET_FIRST_FLEET(stateChanged, users[0]);
    Vector<> position = fleet.position;
    READ_MESSAGE(StateChanged, sockets[1], stateChanged);

    // A legközelebbi bolygó megkeresése
    Closer comp;
    comp.position = fleet.position;
    StateChanged::StarInfo closestStar = *min_element(
      stateChanged->galaxyInfo.stars.begin(),
      stateChanged->galaxyInfo.stars.end(),
      comp
    );
    StateChanged::PlanetInfo planet = *closestStar.planets.begin();

    // Kolonizálás
    {
        FleetColonizePlanet fcp(
          fleet.id,
          closestStar.id,
          planet.id
        );
        fcp.send_to(sockets[0]);
        spDone done;
        READ_MESSAGE(Done, sockets[0], done);
        QCOMPARE(done->status, true);
        QCOMPARE(done->message, QString("game.fleet_colonize_planet ok"));
    }

    // Megvárjuk amíg odaér
    while (fleet.colonyship) {
        NEXT_TURN(sockets[0]);
        NEXT_TURN(sockets[1]);
        spStateChanged stateChanged;
        READ_MESSAGE(StateChanged, sockets[0], stateChanged);
        fleet = GET_FIRST_FLEET(stateChanged, users[0]);
        closestStar = stateChanged->galaxyInfo.stars.value(closestStar.id);
        planet = closestStar.planets.value(planet.id);
        f8 newd = (closestStar.position - fleet.position).get_length();
        f8 oldd = (closestStar.position - position).get_length();
        QVERIFY(newd < oldd || fleet.colonyship == 0);
        position = fleet.position;
        READ_MESSAGE(StateChanged, sockets[1], stateChanged);
    }

    QCOMPARE(planet.owner, users[0]);
    QVERIFY(planet.population > 0);

    DISCONNECT_FROM_SERVER(sockets[0]);
    DISCONNECT_FROM_SERVER(sockets[1]);
}
